13 容器磁盘限速:我的容器里磁盘读写为什么不稳定_

你好,我是程远。今天我们聊一聊容器文件Quota。

上一讲,我们学习了容器文件系统OverlayFS,这个OverlayFS有两层,分别是lowerdir和upperdir。lowerdir里是容器镜像中的文件,对于容器来说是只读的;upperdir存放的是容器对文件系统里的所有改动,它是可读写的。

从宿主机的角度看,upperdir就是一个目录,如果容器不断往容器文件系统中写入数据,实际上就是往宿主机的磁盘上写数据,这些数据也就存在于宿主机的磁盘目录中。

当然对于容器来说,如果有大量的写操作是不建议写入容器文件系统的,一般是需要给容器挂载一个volume,用来满足大量的文件读写。

但是不能避免的是,用户在容器中运行的程序有错误,或者进行了错误的配置。

比如说,我们把log写在了容器文件系统上,并且没有做log rotation,那么时间一久,就会导致宿主机上的磁盘被写满。这样影响的就不止是容器本身了,而是整个宿主机了。

那对于这样的问题,我们该怎么解决呢?

问题再现

我们可以自己先启动一个容器,一起试试不断地往容器文件系统中写入数据,看看是一个什么样的情况。

用Docker启动一个容器后,我们看到容器的根目录(/)也就是容器文件系统OverlayFS,它的大小是160G,已经使用了100G。其实这个大小也是宿主机上的磁盘空间和使用情况。

img

这时候,我们可以回到宿主机上验证一下,就会发现宿主机的根目录(/)的大小也是160G,同样是使用了100G。

img

好,那现在我们再往容器的根目录里写入10GB的数据。

这里我们可以看到容器的根目录使用的大小增加了,从刚才的100G变成现在的110G。而多写入的10G大小的数据,对应的是test.log这个文件。

img

接下来,我们再回到宿主机上,可以看到宿主机上的根目录(/)里使用的大小也是110G了。

img

我们还是继续看宿主机,看看OverlayFS里upperdir目录中有什么文件?

这里我们仍然可以通过/proc/mounts这个路径,找到容器OverlayFS对应的lowerdir和upperdir。因为写入的数据都在upperdir里,我们就只要看upperdir对应的那个目录就行了。果然,里面存放着容器写入的文件test.log,它的大小是10GB。

img

通过这个例子,我们已经验证了在容器中对于OverlayFS中写入数据,其实就是往宿主机的一个目录(upperdir)里写数据。我们现在已经写了10GB的数据,如果继续在容器中写入数据,结果估计你也知道了,就是会写满宿主机的磁盘。

那遇到这种情况,我们该怎么办呢?

知识详解

容器写自己的OverlayFS根目录,结果把宿主机的磁盘写满了。发生这个问题,我们首先就会想到需要对容器做限制,限制它写入自己OverlayFS的数据量,比如只允许一个容器写100MB的数据。

不过我们实际查看OverlayFS文件系统的特性,就会发现没有直接限制文件写入量的特性。别担心,在没有现成工具的情况下,我们只要搞懂了原理,就能想出解决办法。

所以我们再来分析一下OverlayFS,它是通过lowerdir和upperdir两层目录联合挂载来实现的,lowerdir是只读的,数据只会写在upperdir中。

那我们是不是可以通过限制upperdir目录容量的方式,来限制一个容器OverlayFS根目录的写入数据量呢?

沿着这个思路继续往下想,因为upperdir在宿主机上也是一个普通的目录,这样就要看宿主机上的文件系统是否可以支持对一个目录限制容量了。

对于Linux上最常用的两个文件系统XFS和ext4,它们有一个特性Quota,那我们就以XFS文件系统为例,学习一下这个Quota概念,然后看看这个特性能不能限制一个目录的使用量。

XFS Quota

在Linux系统里的XFS文件系统缺省都有Quota的特性,这个特性可以为Linux系统里的一个用户(user),一个用户组(group)或者一个项目(project)来限制它们使用文件系统的额度(quota),也就是限制它们可以写入文件系统的文件总量。

因为我们的目标是要限制一个目录中总体的写入文件数据量,那么显然给用户和用户组限制文件系统的写入数据量的模式,并不适合我们的这个需求。

因为同一个用户或者用户组可以操作多个目录,多个用户或者用户组也可以操作同一个目录,这样对一个用户或者用户组的限制,就很难用来限制一个目录。

那排除了限制用户或用户组的模式,我们再来看看Project模式。Project模式是怎么工作的呢?

我举一个例子你会更好理解,对Linux熟悉的同学可以一边操作,一边体会一下它的工作方式。不熟悉的同学也没关系,可以重点关注我后面的讲解思路。

首先我们要使用XFS Quota特性,必须在文件系统挂载的时候加上对应的Quota选项,比如我们目前需要配置Project Quota,那么这个挂载参数就是”pquota”。

对于根目录来说,这个参数必须作为一个内核启动的参数”rootflags=pquota”,这样设置就可以保证根目录在启动挂载的时候,带上XFS Quota的特性并且支持Project模式。

我们可以从/proc/mounts信息里,看看根目录是不是带”prjquota”字段。如果里面有这个字段,就可以确保文件系统已经带上了支持project模式的XFS quota特性。

img

下一步,我们还需要给一个指定的目录打上一个Project ID。这个步骤我们可以使用XFS文件系统自带的工具 xfs_quota 来完成,然后执行下面的这个命令就可以了。

执行命令之前,我先对下面的命令和输出做两点解释,让你理解这个命令的含义。

第一点,新建的目录/tmp/xfs_prjquota,我们想对它做Quota限制。所以在这里要对它打上一个Project ID。

第二点,通过xfs_quota这条命令,我们给/tmp/xfs_prjquota打上Project ID值101,这个101是我随便选的一个数字,就是个ID标识,你先有个印象。在后面针对Project进行Quota限制的时候,我们还会用到这个ID。

# mkdir -p  /tmp/xfs_prjquota
# xfs_quota -x -c 'project -s -p /tmp/xfs_prjquota 101' /
Setting up project 101 (path /tmp/xfs_prjquota)...
Processed 1 (/etc/projects and cmdline) paths for project 101 with recursion depth infinite (-1).

最后,我们还是使用xfs_quota命令,对101(我们刚才建立的这个Project ID)做Quota限制。

你可以执行下面这条命令,里面的”-p bhard=10m 101”就代表限制101这个project ID,限制它的数据块写入量不能超过10MB。

# xfs_quota -x -c 'limit -p bhard=10m 101' /

做好限制之后,我们可以尝试往/tmp/xfs_prjquota写数据,看看是否可以超过10MB。比如说,我们尝试写入20MB的数据到/tmp/xfs_prjquota里。

我们可以看到,执行dd写入命令,就会有个出错返回信息”No space left on device”。这表示已经不能再往这个目录下写入数据了,而最后写入数据的文件test.file大小也停留在了10MB。

# dd if=/dev/zero of=/tmp/xfs_prjquota/test.file bs=1024 count=20000
dd: error writing '/tmp/xfs_prjquota/test.file': No space left on device
10241+0 records in
10240+0 records out
10485760 bytes (10 MB, 10 MiB) copied, 0.0357122 s, 294 MB/s

# ls -l /tmp/xfs_prjquota/test.file
-rw-r--r-- 1 root root 10485760 Oct 31 10:00 /tmp/xfs_prjquota/test.file

好了,做到这里,我们发现使用XFS Quota的Project模式,确实可以限制一个目录里的写入数据量,它实现的方式其实也不难,就是下面这两步。

第一步,给目标目录打上一个Project ID,这个ID最终是写到目录对应的inode上。

这里我解释一下,inode是文件系统中用来描述一个文件或者一个目录的元数据,里面包含文件大小,数据块的位置,文件所属用户/组,文件读写属性以及其他一些属性。

那么一旦目录打上这个ID之后,在这个目录下的新建的文件和目录也都会继承这个ID。

第二步,在XFS文件系统中,我们需要给这个project ID设置一个写入数据块的限制。

有了ID和限制值之后,文件系统就可以统计所有带这个ID文件的数据块大小总和,并且与限制值进行比较。一旦所有文件大小的总和达到限制值,文件系统就不再允许更多的数据写入了。

用一句话概括,XFS Quota就是通过前面这两步限制了一个目录里写入的数据量。

解决问题

我们理解了XFS Quota对目录限流的机制之后,再回到我们最开始的问题,如何确保容器不会写满宿主机上的磁盘。

你应该已经想到了,方法就是对OverlayFS的upperdir目录做XFS Quota的限流,没错,就是这个解决办法!

其实Docker也已经实现了限流功能,也就是用XFS Quota来限制容器的OverlayFS大小。

我们在用 docker run 启动容器的时候,加上一个参数 --storage-opt size= <SIZE> ,就能限制住容器OverlayFS文件系统可写入的最大数据量了。

我们可以一起试一下,这里我们限制的size是10MB。

进入容器之后,先运行 df -h 命令,这时候你可以看到根目录(/)overlayfs文件系统的大小就10MB,而不是我们之前看到的160GB的大小了。这样容器在它的根目录下,最多只能写10MB数据,就不会把宿主机的磁盘给写满了。

img

完成了上面这个小试验之后,我们可以再看一下Docker的代码,看看它的实现是不是和我们想的一样。

Docker里SetQuota()函数就是用来实现XFS Quota 限制的,我们可以看到它里面最重要的两步,分别是 setProjectIDsetProjectQuota

其实,这两步做的就是我们在基本概念中提到的那两步:

第一步,给目标目录打上一个Project ID;第二步,为这个Project ID在XFS文件系统中,设置一个写入数据块的限制。

// SetQuota - assign a unique project id to directory and set the quota limits
// for that project id

func (q *Control) SetQuota(targetPath string, quota Quota) error {
        q.RLock()
        projectID, ok := q.quotas[targetPath]
        q.RUnlock()

        if !ok {
                q.Lock()
                projectID = q.nextProjectID

                //
                // assign project id to new container directory
                //

                err := setProjectID(targetPath, projectID)
                if err != nil {
                        q.Unlock()
                        return err
                }

                q.quotas[targetPath] = projectID
                q.nextProjectID++
                q.Unlock()
        }

 

        //
        // set the quota limit for the container's project id
        //

        logrus.Debugf("SetQuota(%s, %d): projectID=%d", targetPath, quota.Size, projectID)
        return setProjectQuota(q.backingFsBlockDev, projectID, quota)
}

setProjectIDsetProjectQuota 是如何实现的呢?

你可以进入到这两个函数里看一下,它们分别调用了ioctl()和quotactl()这两个系统调用来修改内核中XFS的数据结构,从而完成project ID的设置和Quota值的设置。具体的细节,我不在这里展开了,如果你有兴趣,可以继续去查看内核中对应的代码。

好了,Docker里XFS Quota操作的步骤完全和我们先前设想的一样,那么还有最后一个问题要解决,XFS Quota限制的目录是哪一个?

这个我们可以根据/proc/mounts中容器的OverlayFS Mount信息,再结合Docker的代码,就可以知道限制的目录是”/var/lib/docker/overlay2/“。那这个目录下有什么呢?果然upperdir目录中有对应的”diff”目录,就在里面!

img

讲到这里,我想你已经清楚了对于使用OverlayFS的容器,我们应该如何去防止它把宿主机的磁盘给写满了吧?方法就是对OverlayFS的upperdir目录做XFS Quota的限流。

重点总结

我们这一讲的问题是,容器写了大量数据到OverlayFS文件系统的根目录,在这个情况下,就会把宿主机的磁盘写满。

由于OverlayFS自己没有专门的特性,可以限制文件数据写入量。这时我们通过实际试验找到了解决思路:依靠底层文件系统的Quota特性来限制OverlayFS的upperdir目录的大小,这样就能实现限制容器写磁盘的目的。

底层文件系统XFS Quota的Project模式,能够限制一个目录的文件写入量,这个功能具体是通过这两个步骤实现:

第一步,给目标目录打上一个Project ID。

第二步,给这个Project ID在XFS文件系统中设置一个写入数据块的限制。

Docker正是使用了这个方法,也就是用XFS Quota来限制OverlayFS的upperdir目录,通过这个方式控制容器OverlayFS的根目录大小。

当我们理解了这个方法后,对于不是用Docker启动的容器,比如直接由containerd启动起来的容器,也可以自己实现XFS Quota限制upperdir目录。这样就能有效控制容器对OverlayFS的写数据操作,避免宿主机的磁盘被写满。

思考题

在正文知识详解的部分,我们使用”xfs_quota”给目录打了project ID并且限制了文件写入的数据量。那在做完这样的限制之后,我们是否能用xfs_quota命令,查询到被限制目录的project ID和限制的数据量呢?

欢迎你在留言区分享你的思考或疑问。如果这篇文章让你有所收获,也欢迎转发给你的同事、朋友,一起交流和学习。